package com.spa.infrastructure.service.douyin;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spa.infrastructure.enums.dict.base.SourceTypeEnum;
import com.spa.infrastructure.exception.SpaException;
import com.spa.infrastructure.service.HttpService;
import com.spa.infrastructure.service.douyin.param.ClientTokenParam;
import com.spa.infrastructure.service.douyin.param.GetAccessTokenParam;
import com.spa.infrastructure.service.douyin.result.*;
import com.spa.infrastructure.util.RedisUtil;
import com.spa.infrastructure.util.StringUtils;
import com.spa.infrastructure.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

@Service
@Slf4j
public class DouYinService {

    public static final String ACCESS_TOKEN = "getAccessToken";
    private static final String CLIENT_TOKEN = "client_token";

    @Autowired
    private HttpService httpService;

    @Autowired
    public RedisUtil redisUtil;

    @Autowired
    public ObjectMapper mapper;

    /**
     * 平台公钥
     */
    @Value("${douYin.mini.publicKey}")
    private String publicKey;

    /**
     * 应用私钥
     */
    @Value("${douYin.mini.privateKey}")
    private String privateKey;

    /**
     * 应用公钥版本,可通过「开发管理-开发设置-密钥设置」处获取
     */
    @Value("${douYin.mini.keyVersion}")
    private String keyVersion;

    @Value("${douYin.mini.appId}")
    private String appId;

    public String getAccessToken(String appId, String secret) {
        String redisKey = SourceTypeEnum.DouYin.getCode() +"-"+ACCESS_TOKEN;
        String accessToken = redisUtil.get(redisKey);
        if (StringUtils.isNotEmpty(accessToken)) {
            return accessToken;
        }
        GetAccessTokenParam param = GetAccessTokenParam.builder()
                .appid(appId)
                .grant_type("client_credential")
                .secret(secret)
                .build();
        String result = sendPost4DouYinV2(DouYinConstant.GET_ACCESS_TOKEN, JSONUtil.toJsonStr(param));
        GetAccessTokenResult getAccessTokenResult = JSONUtil.toBean(result, GetAccessTokenResult.class);
        log.info("GetAccessTokenResult : {}", getAccessTokenResult);
        accessToken = getAccessTokenResult.getAccess_token();
        long timeout = getAccessTokenResult.getExpires_in();
        redisUtil.set(redisKey, accessToken, timeout - 60);
        return accessToken;
    }

    /**
     * 生成 client_token
     * client_token 用于不需要用户授权就可以调用的接口。
     * client_token 的有效时间为 2 个小时，重复获取 client_token 后会使上次的 client_token 失效（但有 5 分钟的缓冲时间，连续多次获取 client_token 只会保留最新的两个 client_token）。
     * @return
     */
    public String getClientToken(String appId, String secret, SourceTypeEnum sourceTypeEnum) {
        String redisKey = sourceTypeEnum.getCode() +"-"+CLIENT_TOKEN;
        String accessToken = redisUtil.get(redisKey);
        if (StringUtils.isNotEmpty(accessToken)) {
            return accessToken;
        }
        ClientTokenParam param = ClientTokenParam.builder()
                .client_key(appId)
                .grant_type("client_credential")
                .client_secret(secret)
                .build();
        String result = sendPost4DouYinV2(DouYinConstant.CLIENT_TOKEN, JSONUtil.toJsonStr(param));
        ClientTokenResult clientTokenResult = JSONUtil.toBean(result, ClientTokenResult.class);
        log.info("ClientTokenResult : {}", clientTokenResult);
        if (clientTokenResult.getData().getError_code() != 0) {
            throw new SpaException(clientTokenResult.getData().getDescription());
        }
        accessToken = clientTokenResult.getData().getAccess_token();
        long timeout = clientTokenResult.getData().getExpires_in();
        redisUtil.set(redisKey, accessToken, timeout - 60);
        return accessToken;
    }

    public String sendPost4DouYin(String domain, String uri, String param) {
        log.info("domain : {}, url : {}, param : {}", domain, uri, param);
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("content-type", "application/json");
        requestHeaders.set("Byte-Authorization", getPostHeader(uri, param));
        String result = httpService.sendPost(domain + uri, requestHeaders, param);
        log.info("result : {}", result);
        DouYinResult sessionResult = JSONUtil.toBean(result, DouYinResult.class);
        if (sessionResult.getErr_no() != 0) {
            throw new SpaException(sessionResult.getErr_tips());
        }
        return sessionResult.getData();
    }

    public String sendPost4DouYinV2(String url, String param) {
        log.info("domain : {}, url : {}", url, param);
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("content-type", "application/json");
        String result = httpService.sendPost(url, requestHeaders, param);
        log.info("result : {}", result);
        return result;
    }

    public String sendPost4DouYinV3(String url, String param, String token) {
        log.info("domain : {}, url : {} ", url, param);
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("content-type", "application/json");
        requestHeaders.set("access-token", token);
        String result = httpService.sendPost(url, requestHeaders, param);
        log.info("result : {}", result);
        return result;
    }

    public String sendPost4DouYinV4(String url, String param) {
        log.info("domain : {}, url : {} ", url, param);
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("content-type", "application/x-www-form-urlencoded");
        String result = httpService.sendPost(url, requestHeaders, param);
        log.info("result : {}", result);
        return result;
    }

    public String sendGet4DouYin(String domain, String uri) {
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("Byte-Authorization", getGetHeader(uri));
        return httpService.sendGet(domain + uri, requestHeaders);
    }

    public String sendGet4DouYinV2(String url, String token) {
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("Content-Type", "application/json");
        requestHeaders.set("access-token", token);
        log.info("url : {}, token : {}, requestHeaders {} ", url, token, requestHeaders);
        String response = httpService.sendGet(url, requestHeaders);
        log.info("response {}", response);
        return response;
    }

    public String sendGet4DouYinV3(String url) {
        log.info("url : {} ", url);
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("content-type", "application/json");
        return httpService.sendGet(url, requestHeaders);
    }

    public String sendGet4DouYinV4(String url, String token) {
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("Content-Type", "application/x-www-form-urlencoded");
        requestHeaders.set("access-token", token);
        log.info("url : {}, token : {}, requestHeaders {} ", url, token, requestHeaders);
        String response = httpService.sendGet(url, requestHeaders);
        log.info("response {}", response);
        return response;
    }

    private String getGetHeader(String uri) {
        try {
            return getHeader("GET", uri, "");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private String getPostHeader(String uri, String body) {
        try {
            return getHeader("POST", uri, body);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * HTTP 请求方法
     * 如：POST，GET，PUT 等。注意请使用大写。
     * URI
     * 获取请求的开放平台接口的绝对 URL，并去除域名部分得到参与签名的 URI。 URI 必须以斜杠字符“/”开头。 如
     * PATH=https://open.douyin.com/api/trade/v2/query URI 则为 /api/trade/v2/query
     * PATH=https://open.douyin.com/api/trade/v2/query?a=x URI 则为 /api/trade/v2/query?a=x
     *
     * 请求时间戳
     * 发起请求时系统的当前时间戳，即格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数，作为请求时间戳。「开放平台」会拒绝处理一个小时之前发起的请求。
     * 请求随机串
     * 任意生成一个随机字符串（长度不限制），以保证相同时间相同参数发起的请求签名值不一样。
     * 请求报文主体
     * 获取请求中的请求报文主体（request body）。
     * 请求方法为GET时，报文主体为空。
     * 当请求方法为POST或PUT时，请使用报文内容。
     * @param method
     * @param uri
     * @param timestamp
     * @param nonce
     * @param body
     * @return
     * @throws Exception
     */
    private String getSignature(String method, String uri, long timestamp, String nonce, String body) throws Exception {
        //构造待签名串
        String rawStr = method + "\n" +
                uri + "\n" +
                timestamp + "\n" +
                nonce + "\n" +
                body + "\n";
        //使用应用私钥对待签名串进行SHA256-RSA2048签名，并对签名结果进行Base64编码得到签名值
        Signature sign = Signature.getInstance(DouYinConstant.SIGN_ALGORITHM);
        sign.initSign(string2PrivateKey());
        sign.update(rawStr.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(sign.sign());
    }

    private PrivateKey string2PrivateKey() {
        PrivateKey prvKey = null;
        try {
            String key = privateKey;
            byte[] privateBytes = Base64.getDecoder().decode(key);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            prvKey = keyFactory.generatePrivate(keySpec);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return prvKey;
    }

    /**
     * 设置 Header Byte-Authorization
     * @param method
     * @param uri
     * @param body
     * @return
     */
    private String getHeader(String method, String uri, String body) throws Exception {
        long timestamp = DateUtil.currentSeconds();
        String nonce = UUID.fastUUID().toString(true);
        String sign = getSignature(method, uri, timestamp, nonce, body);
        StringBuilder sb = new StringBuilder();
        sb.append(DouYinConstant.AUTHENTICATION_TYPE);
        sb.append(" ");
        sb.append("appid=\"");
        sb.append(appId);
        sb.append("\",");
        sb.append("nonce_str=\"");
        sb.append(nonce);
        sb.append("\",");
        sb.append("timestamp=\"");
        sb.append(timestamp);
        sb.append("\",");
        sb.append("key_version=\"");
        sb.append(keyVersion);
        sb.append("\",");
        sb.append("signature=\"");
        sb.append(sign);
        sb.append("\"");
        return sb.toString();
    }

    /**
     * 验签方法
     * @param httpBody
     * @param signStr
     * @param timestamp
     * @param nonce
     * @return
     * @throws Exception
     */
    public boolean verify(String httpBody, String signStr, Long timestamp, String nonce) throws Exception {
        StringBuffer buffer = new StringBuffer();
        buffer.append(timestamp).append("\n");
        buffer.append(nonce).append("\n");
        buffer.append(httpBody).append("\n");
        String message = buffer.toString();
        Signature sign = Signature.getInstance(DouYinConstant.SIGN_ALGORITHM);
        sign.initVerify(string2PublicKey()); // 注意验签时publicKey使用平台公钥而非应用公钥
        sign.update(message.getBytes(StandardCharsets.UTF_8));
        return sign.verify(Base64.getDecoder().decode(signStr.getBytes(StandardCharsets.UTF_8)));
    }

    private PublicKey string2PublicKey() throws Exception{
        byte[] decoded = Base64.getDecoder().decode(publicKey);
        return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
    }

}
